Skip to content

feat(cli): honest non-interactive init — fail on skew, no false success (rc.2 M4)#687

Merged
coderdan merged 3 commits into
mainfrom
feat/honest-noninteractive-init
Jul 19, 2026
Merged

feat(cli): honest non-interactive init — fail on skew, no false success (rc.2 M4)#687
coderdan merged 3 commits into
mainfrom
feat/honest-noninteractive-init

Conversation

@coderdan

@coderdan coderdan commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Problem (rc.2 M4)

Non-interactive stash init reported success for setups that hadn't fully completed — so CI/agents couldn't trust its exit code or its summary. Three honesty gaps:

  1. Version skew was warn-and-proceed. A non-interactive run can't reconcile a behind skew (an installed @cipherstash/* older than this CLI expects) — it won't mutate an install without consent — so it warned and kept going, scaffolding config/client against mismatched packages, then printed "Setup complete".
  2. "Setup complete" was unconditional. Even when EQL install failed (eqlInstalled=false), the header still said complete — and "✓ Database connection verified" was printed although init only resolves a URL, never connects.
  3. "Skills loaded" was unconditional. The Claude/Codex handoff launch prompt pointed the agent at the skills dir even when installSkills() copied nothing (stripped build) — telling it to read files that aren't there.

Fix

  1. Fail on skew. Non-interactive + behind skew → refuse with a non-zero exit (CliExit(1)) and the exact align command, instead of proceeding. The npm dist-tags deliver broken/stale packages: adapters' latest=0.0.0, stack latest=0.19.0; stash init installs unpinned #661/fix(cli): pin init's installs to the release's own package versions (#661) #666 no-mutation principle is preserved (execSync still never called — only the post-warning action changes). ahead skew stays a warn (install likely fine; update the CLI). Interactive runs still offer to align.
  2. Honest summary. When EQL is required but missing (!eqlInstalled && integration !== 'prisma-next'), the summary reads "Setup incomplete" and init exits non-zero, pointing at stash eql install. "Database connection verified" → "Database URL resolved"; "Encryption client scaffolded" shows only when a client was actually written (skipped for Prisma Next).
  3. Honest skills prose. The launch-prompt skills clause is conditional on installSkills() returning something.

Scope notes

Verification

  • Updated install-deps skew tests (refuses + exit 1, still never mutates); new init-command honest-summary tests (EQL-missing → exit 1 + "Setup incomplete"; prisma-next eqlInstalled=false → completes).
  • Suite: 542 unit / 62 e2e green, code:check clean. Changeset: stash minor. Skill (stash-cli) + doc comments updated.
  • No new e2e: a full non-interactive init needs live auth + a DB (which is why there's no init e2e); the honest-summary and skew-fail logic is unit-covered.

https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w

Summary by CodeRabbit

  • Bug Fixes
    • stash init in non-interactive mode now fails (“Setup incomplete” / non-zero exit) when EQL setup is missing or when dependency version skew can’t be safely reconciled, including printing the exact alignment command.
    • Completion summaries now more accurately reflect encryption client status (scaffolded vs kept) and EQL state (installed vs migration generated/pending apply).
    • Claude and Codex handoff prompts no longer mention .*/skills/ when skills weren’t installed.
  • Documentation
    • Updated stash init documentation to clarify exact version pinning, honest non-interactive mismatch behavior, and EQL/migration status expectations.

@coderdan
coderdan requested a review from a team as a code owner July 17, 2026 08:57
@changeset-bot

changeset-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 20e5df9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
stash Minor
@cipherstash/basic-example Patch
@cipherstash/e2e Patch
@cipherstash/stack Minor
@cipherstash/stack-drizzle Minor
@cipherstash/stack-supabase Minor
@cipherstash/prisma-next Minor
@cipherstash/wizard Minor
@cipherstash/bench Patch
@cipherstash/test-kit Patch
@cipherstash/prisma-next-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

stash init now exits non-zero for non-interactive dependency skew and incomplete EQL setup, reports completion from actual state, distinguishes generated migrations, and avoids mentioning absent agent skills in handoff prompts.

Changes

Init outcome handling

Layer / File(s) Summary
Dependency skew refusal
packages/cli/src/messages.ts, packages/cli/src/commands/init/steps/install-deps.ts, packages/cli/src/commands/init/steps/__tests__/install-deps.test.ts
Non-interactive dependency skew prints alignment commands, logs the refusal message, exits with CliExit, and avoids installation commands.
EQL installation outcome tracking
packages/cli/src/commands/db/install.ts, packages/cli/src/commands/init/types.ts, packages/cli/src/commands/init/steps/install-eql.ts, packages/cli/src/commands/init/steps/__tests__/install-eql.test.ts
EQL installation reports installed, already-installed, migration-generated, or dry-run outcomes; init records pending migrations separately from installed EQL.
Init completion validation
packages/cli/src/commands/init/index.ts, packages/cli/src/commands/init/__tests__/init-command.test.ts, .changeset/honest-noninteractive-init.md, skills/stash-cli/SKILL.md
Completion checkmarks reflect actual state; missing EQL reports incomplete setup and exits non-zero except for Prisma Next, while generated migrations show integration-specific apply guidance.
Conditional agent handoff prompts
packages/cli/src/commands/impl/steps/*, packages/cli/src/commands/init/lib/*
Claude and Codex prompts reference skills directories and rules sources only when those files were written, with coverage for plan and implement modes.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant initCommand
  participant installDepsStep
  participant installEqlStep
  participant installCommand
  participant AgentHandoff
  User->>initCommand: run stash init
  initCommand->>installDepsStep: check dependency versions
  installDepsStep-->>initCommand: dependency state or CliExit
  initCommand->>installEqlStep: install EQL
  installEqlStep->>installCommand: request installation
  installCommand-->>installEqlStep: installed or migration-generated
  installEqlStep-->>initCommand: EQL state
  initCommand->>AgentHandoff: build prompt from installed skills
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: calvinbrewer

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: non-interactive init now fails on skew and avoids false success messaging.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/honest-noninteractive-init

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/impl/steps/handoff-claude.ts`:
- Around line 41-48: Make all generated handoff guidance respect the
installed-skills state: in handoff-claude.ts, update the setup prompt loaded by
the launchPrompt flow so the empty-skills variant does not reference skill
directories or the unwritten AGENTS.md file, while retaining valid skill
guidance when skills exist; in handoff-codex.ts, update its generated guidance
so .codex/skills/ is omitted when installed.length is zero.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8da61ce-6475-4788-b2d3-6295b53bb730

📥 Commits

Reviewing files that changed from the base of the PR and between f188c7a and 27520d6.

📒 Files selected for processing (9)
  • .changeset/honest-noninteractive-init.md
  • packages/cli/src/commands/impl/steps/handoff-claude.ts
  • packages/cli/src/commands/impl/steps/handoff-codex.ts
  • packages/cli/src/commands/init/__tests__/init-command.test.ts
  • packages/cli/src/commands/init/index.ts
  • packages/cli/src/commands/init/steps/__tests__/install-deps.test.ts
  • packages/cli/src/commands/init/steps/install-deps.ts
  • packages/cli/src/messages.ts
  • skills/stash-cli/SKILL.md

Comment thread packages/cli/src/commands/impl/steps/handoff-claude.ts
…ss (rc.2 M4)

Non-interactive `stash init` reported success for setups that didn't fully
complete. Three honesty gaps closed:

1. Version skew. A non-interactive run can't reconcile a `behind` skew (it
   won't mutate an install without consent), so instead of warning-and-
   proceeding — scaffolding against packages older than this CLI expects and
   then claiming success — it now REFUSES with a non-zero exit (CliExit(1))
   and the exact align command. The #661/#666 no-mutation principle is
   preserved (execSync still not called); only the post-warning action
   changes from proceed to fail. `ahead` skew stays a warn (install likely
   fine; update the CLI).

2. False 'Setup complete'. The summary header and the auth/db/scaffold
   checkmarks were unconditional. Now: when EQL is required but not installed
   (eqlInstalled=false AND integration !== prisma-next), the summary reads
   'Setup incomplete' and init exits non-zero, pointing at `stash eql install`.
   'Database connection verified' → 'Database URL resolved' (init resolves a
   URL, never opens a connection). 'Encryption client scaffolded' is shown
   only when a client was written (clientFilePath set; skipped for prisma-next).

3. False 'skills loaded'. The Claude/Codex handoff launch prompt referenced
   the skills dir unconditionally; a stripped build installs no skills, so it
   told the agent to read files that aren't there. The clause is now
   conditional on installSkills() actually copying something.

Tests: install-deps skew tests updated to assert the refusal (still never
mutates); new init-command honest-summary tests (EQL-missing → exit 1 +
'Setup incomplete'; prisma-next eqlInstalled=false → completes). Skill + doc
comments updated. Suite 542 unit / 62 e2e green, code:check clean. Changeset:
stash minor.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
@coderdan
coderdan force-pushed the feat/honest-noninteractive-init branch from 27520d6 to fb77e14 Compare July 18, 2026 12:26
@coderdan
coderdan requested a review from Copilot July 18, 2026 15:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes stash init reliable for automation by ensuring non-interactive runs never report success when setup is incomplete or potentially inconsistent with the CLI’s expected runtime package versions.

Changes:

  • Make non-interactive init refuse (exit non-zero) on behind version skew instead of warning-and-proceeding.
  • Make the init summary truthful: “Database URL resolved” (not “connection verified”), “Setup incomplete” + exit non-zero when EQL is missing (except Prisma Next), and only reference skills when they were actually installed.
  • Update unit coverage, changeset, and the published stash-cli skill documentation to match the new behavior.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
skills/stash-cli/SKILL.md Documents the new “fail on behind skew” and “setup incomplete” non-interactive behavior.
packages/cli/src/messages.ts Adds stable message leaders for non-interactive init failure modes.
packages/cli/src/commands/init/steps/install-deps.ts Refuses non-interactively on behind skew (throws CliExit(1)), while keeping ahead skew as warn-and-proceed.
packages/cli/src/commands/init/steps/tests/install-deps.test.ts Updates skew tests to assert refusal + no mutation in non-interactive mode.
packages/cli/src/commands/init/index.ts Adjusts checkmark copy, adds EQL-missing “Setup incomplete” gate + non-zero exit (except Prisma Next).
packages/cli/src/commands/init/tests/init-command.test.ts Adds unit tests asserting the honest-summary behavior and Prisma Next exception.
packages/cli/src/commands/impl/steps/handoff-codex.ts Makes the “skills” clause in the launch prompt conditional on actual skill installation.
packages/cli/src/commands/impl/steps/handoff-claude.ts Makes the “skills” clause in the launch prompt conditional on actual skill installation.
.changeset/honest-noninteractive-init.md Adds release notes describing the non-interactive honesty changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/cli/src/commands/init/index.ts Outdated
Two "only claim what actually happened" gaps found in review:

- Copilot: the "✓ Encryption client scaffolded" checkmark was gated on
  `state.clientFilePath`, which build-schema sets even when it KEEPS an existing
  file (writes nothing). Gate on `state.schemaGenerated` (true only when a
  placeholder was written); show "✓ Encryption client kept (existing file)" for
  the keep path. Prisma Next (no clientFilePath) still shows neither.

- CodeRabbit: the setup-prompt "Skills loaded" section pointed at skill
  directories and `AGENTS.md` regardless of what the handoff wrote — so a
  stripped build (no skills) sent the agent to read files that don't exist, and
  the Claude handoff (which never writes AGENTS.md) named it as the doctrine
  source. New `skillsLoadedLines(handoff, installedSkills)` renders the section
  honestly: no skills → point only at what was written (AGENTS.md for
  codex/agents-md, the docs for claude-code); claude-code → doctrine is in the
  installed skills, not AGENTS.md. Also hedged "list the skills loaded (if any)".

Added setup-prompt honesty tests (empty-skills × handoff) and de-duped the two
identical inline blocks. 557 tests green; build + biome clean.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
@coderdan

coderdan commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Differential Review: PR #687

Agent: Codex/gpt-5.6-sol

Executive Summary

Severity Count
Critical 0
High 1
Medium 0
Low 0

Overall risk: High

Recommendation: Changes requested. The new completion gate still reports a
successful, complete setup for the supported Drizzle migration path before EQL
has been applied to the database.

Key metrics:

  • Files analyzed: 11/11 changed files
  • Commits analyzed: 2
  • Changed lines: +259/-32
  • Test gap: no test joins the Drizzle installEqlStep path to the new final
    completion gate
  • Security regressions detected: 0

What Changed

Commit range: 4923c0ade0daee25ec89a8bd5c6aa0ea7022ffcb..1d245a9ac1c8f8813a7b25859dd34aea4a1e9e45

Timeline: 2026-07-17 to 2026-07-18

Area Risk Summary
Init completion status Medium Adds a non-zero exit when EQL is pending and makes summary checkmarks conditional
Dependency skew Medium Refuses non-interactive runs with older installed release-train packages
Agent handoff prompts Low Omits skill-directory guidance when no skills were copied
Tests, skill, changeset Low Documents and exercises the intended behavior

Findings

High: Drizzle still exits successfully before EQL is installed

File: packages/cli/src/commands/init/index.ts:130

Commit: fb77e148de259daca23829c8d221948408b88ba4

Blast radius: Every stash init --drizzle run (and auto-detected Drizzle
run); one of the four supported integrations.

Test coverage: Missing end-to-end state coverage. The new init test injects
eqlInstalled directly, while the install-eql tests mock installCommand and
do not exercise its migration-only result.

The new gate trusts state.eqlInstalled, but the Drizzle path does not make
that field mean what the gate assumes:

  1. installEqlStep calls installCommand with drizzle: true and
    eqlVersion: '2'.
  2. installCommand routes to generateDrizzleMigration() and returns without
    connecting to or changing the database (packages/cli/src/commands/db/install.ts:232-242).
  3. The generator explicitly says to run drizzle-kit migrate to install EQL
    (packages/cli/src/commands/db/install.ts:593-599).
  4. installEqlStep nevertheless returns eqlInstalled: true
    (packages/cli/src/commands/init/steps/install-eql.ts:118).
  5. The added eqlPending check is therefore false, so init prints
    EQL extension installed, prints Setup complete, and exits 0.

This preserves the exact false-success contract the PR is intended to remove.
It also makes the changeset's fallback command wrong for this case: rerunning
stash eql install generates another migration; the pending action is to apply
the generated Drizzle migration.

Concrete scenario:

  1. A CI job runs non-interactive stash init --drizzle against a database that
    does not contain EQL.
  2. Init generates an EQL migration file but does not apply it.
  3. installEqlStep records eqlInstalled: true.
  4. The new completion check prints success and exits 0.
  5. Automation proceeds to encryption queries, which fail because the EQL types
    and functions are absent from the database.

Recommendation: Make installCommand return a structured outcome such as
'installed' | 'migration-generated' | 'already-installed', and only set
eqlInstalled for the database-applied outcomes. Handle the Drizzle pending
outcome separately in the init summary, with drizzle-kit migrate guidance and
the intended exit policy. Add a test that carries the Drizzle outcome through
installEqlStep into initCommand rather than injecting the final boolean.

Test Coverage Analysis

All 557 CLI unit tests and all 65 CLI E2E tests passed. The CLI package build
also passed. Repository-wide code:check completed with no errors (existing
warnings and schema-version informational diagnostics remain).

The important uncovered transition is:

Drizzle migration generated -> installEqlStep state -> initCommand final exit

Current tests cover each endpoint with mocks, which is why the contradictory
state is not detected.

Blast Radius Analysis

Changed behavior Reach Assessment
Final EQL completion gate All init runs High functional reach; incorrect for Drizzle
Non-interactive behind-skew refusal All init dependency checks Medium; focused tests cover refusal and no mutation
Conditional handoff skill prose Claude and Codex handoffs Low; setup-prompt variants are covered

Historical Context

  • The unconditional eqlInstalled: true state was introduced in f36e5834
    when the init EQL step was added.
  • The explicit Drizzle EQL v2 migration routing was added later in e25eb224.
  • PR feat(cli): honest non-interactive init — fail on skew, no false success (rc.2 M4) #687 adds a completion invariant on top of that older boolean without
    reconciling its migration-generated meaning.
  • No removed validation or security-related code was found in this PR.

Recommendations

Blocking

  • Represent and handle the Drizzle migration-generated outcome honestly.
  • Add a cross-layer regression test for the Drizzle completion/exit path.

Follow-up

  • Consider persisting init outcomes in .cipherstash/context.json; plan and
    impl currently reconstruct stackInstalled, cliInstalled, and
    eqlInstalled as true, so later handoff prose cannot distinguish completed
    work from pending migration application.

Analysis Methodology

Strategy: Focused review of a large TypeScript repository (692 source files),
with all changed files reviewed and one-hop tracing through init, EQL install,
CLI exit handling, context persistence, and handoff rendering.

Techniques used:

  • Compared both commits against the merge-base with origin/main
  • Read current and baseline versions of all production changes
  • Inspected git blame/history for the changed completion and skew behavior
  • Traced state and exit flow through initCommand, installDepsStep,
    installEqlStep, and installCommand
  • Checked affected customer-facing skill and changeset text
  • Built the CLI and validated its generated manifest
  • Ran the CLI unit and E2E suites plus repository Biome checks

Limitations: No live database or credentials were used. The Drizzle finding
does not require them because the migration branch returns before database I/O
and explicitly renders the later apply command.

Confidence: High.

@coderdan coderdan left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test-coverage review — stash init honesty changes (PR #687)

The behaviour that carries the exit-code contract is well covered: the non-interactive behind-skew refusal (exit 1, never mutates), the ahead-skew non-interactive non-refusal, the unreadable-manifest → behind path (install-deps.test.ts), and the EQL-missing → "Setup incomplete"/exit-1 gate plus the Prisma-Next exemption (init-command.test.ts). Nice work locking the leaders the e2e suite asserts on.

Three new branches introduced by this diff have no test exercising them:

  1. The encryption-client summary line now forks three ways (scaffolded / kept (existing file) / nothing) and none of the outcomes is asserted.
  2. & 3. The skillsClause empty-vs-populated branch in each agent handoff prompt (handoff-claude.ts, handoff-codex.ts) — the launch-prompt analog of the setup-prompt change that did get a dedicated test. impl.test.ts mocks howToProceedStep.run out, so no test drives either handoff prompt.

Details inline.

Additional coverage gaps not posted inline (minor)

  • install-deps.ts:213 — the refusal test asserts the p.note align commands and exit, but not that p.log.error(messages.init.skewNonInteractive) is emitted (the why message). Low value; the exit-1 is the contract.
  • setup-prompt.ts skillsLoadedLines — the agents-md + no-skills combination isn't tested directly; it shares the wroteAgentsMd branch with codex (which is tested), so it's covered by construction rather than by assertion.

No security concerns noted in this diff.

Comment thread packages/cli/src/commands/init/index.ts
Comment thread packages/cli/src/commands/impl/steps/handoff-claude.ts
Comment thread packages/cli/src/commands/impl/steps/handoff-codex.ts
Differential review (Codex) found the completion gate still reported a
successful, complete setup for `stash init --drizzle` before EQL was in the
database: the Drizzle path only *generates* a v2 migration (applied later with
`drizzle-kit migrate`), yet `installEqlStep` returned `eqlInstalled: true` for
every non-throwing outcome — the exact false-success this PR removes elsewhere.

- `installCommand` now returns a structured `InstallOutcome`
  (`installed` | `already-installed` | `migration-generated` | `dry-run`).
- `installEqlStep` maps `migration-generated` to a new `eqlMigrationPending`
  state (EQL not in the DB yet) instead of `eqlInstalled`.
- The init summary reports it honestly — "○ EQL migration generated — apply it
  with `drizzle-kit migrate`" — and excuses it from the "Setup incomplete"
  hard-fail (exit 0), the same treatment Prisma Next already gets.
- Changeset + stash-cli skill updated to match.

Test-gap review: added the previously-uncovered branches —
- init summary's three-way encryption-client checkmark (scaffolded / kept);
- the Drizzle migration-pending gate (cross-layer: outcome → step → summary);
- handoff-claude / handoff-codex launch-prompt skills clause (stripped build
  drops the skills dir; codex keeps AGENTS.md).

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
@coderdan

Copy link
Copy Markdown
Contributor Author

Addressed the High finding (Drizzle exits successfully before EQL is installed) in 20e5df97.

The gate no longer trusts a boolean that meant two different things. installCommand now returns a structured InstallOutcome'installed' | 'already-installed' | 'migration-generated' | 'dry-run' — and installEqlStep only sets eqlInstalled for the DB-applied outcomes. The Drizzle path (which routes to generateDrizzleMigration and returns without DB I/O) yields 'migration-generated', which maps to a new eqlMigrationPending state.

The init summary now handles that outcome honestly and separately, exactly as recommended:

  • It does not print "✓ EQL extension installed".
  • It prints "○ EQL migration generated — apply it with `drizzle-kit migrate`".
  • It is excused from the eqlPending hard-fail (exit 0), the same treatment Prisma Next already gets — generating the migration is the intended successful end state of stash init --drizzle; applying it is drizzle-kit's job.
  • The changeset's fallback guidance is corrected accordingly (was pointing at stash eql install, which would just generate another migration).

On the test gap: rather than injecting the final boolean, coverage now meets in the middle at eqlMigrationPendinginstall-eql.test.ts proves installCommand returning 'migration-generated' maps to eqlMigrationPending: true / eqlInstalled: false (with eqlVersion: '2' pinned), and init-command.test.ts proves that state produces the honest summary + exit 0. The full-pipeline initCommand seam mocks its steps (native-module loading), so the two-test seam is the cleanest cross-layer coverage without booting the whole pipeline.

Deferred (your follow-up bullet): persisting init outcomes in .cipherstash/context.json so plan/impl can distinguish completed work from pending migration application — worth a separate issue, not blocking here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/cli/src/commands/init/steps/__tests__/install-eql.test.ts (1)

85-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test exercises the real Drizzle chain end-to-end (db/install.tsinstall-eql.tsinit/index.ts). Each layer's test mocks its immediate dependency, so a future contributor could break the InstallOutcomeeqlMigrationPending → completion-gate contract at any hop without a failing test — the same class of bug this PR fixes.

  • packages/cli/src/commands/init/steps/__tests__/install-eql.test.ts#L85-L104: keep this unit test, but add a companion test that calls installEqlStep.run with the real installCommand (mock only its internals — e.g. generateDrizzleMigration, pg/EQLInstaller) to prove the real function actually returns 'migration-generated' for the Drizzle path and that installEqlStep correctly maps it.
  • packages/cli/src/commands/init/__tests__/init-command.test.ts#L118-L141: add a companion test that runs initCommand with the real installEqlStep.run (not eqlRun), mocking only installCommand, to prove the field set by the real step actually satisfies initCommand's completion gate for the Drizzle flow.

Based on the PR's own review comments recommending "adding a cross-layer regression test covering the Drizzle flow through the final init status."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/init/steps/__tests__/install-eql.test.ts` around
lines 85 - 104, The Drizzle regression coverage stops at mocked layer boundaries
and does not verify the real status propagation chain. In
packages/cli/src/commands/init/steps/__tests__/install-eql.test.ts:85-104,
retain the existing unit test and add a companion using the real installCommand
while mocking only its internals, asserting the real Drizzle path returns
migration-generated and installEqlStep maps it to eqlMigrationPending. In
packages/cli/src/commands/init/__tests__/init-command.test.ts:118-141, add a
companion using the real installEqlStep.run while mocking only installCommand,
and assert initCommand's final Drizzle completion status honors the field
produced by the real step.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/cli/src/commands/init/steps/__tests__/install-eql.test.ts`:
- Around line 85-104: The Drizzle regression coverage stops at mocked layer
boundaries and does not verify the real status propagation chain. In
packages/cli/src/commands/init/steps/__tests__/install-eql.test.ts:85-104,
retain the existing unit test and add a companion using the real installCommand
while mocking only its internals, asserting the real Drizzle path returns
migration-generated and installEqlStep maps it to eqlMigrationPending. In
packages/cli/src/commands/init/__tests__/init-command.test.ts:118-141, add a
companion using the real installEqlStep.run while mocking only installCommand,
and assert initCommand's final Drizzle completion status honors the field
produced by the real step.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0544bc31-27b7-4347-acd8-e699865a706d

📥 Commits

Reviewing files that changed from the base of the PR and between 27520d6 and 20e5df9.

📒 Files selected for processing (17)
  • .changeset/honest-noninteractive-init.md
  • packages/cli/src/commands/db/install.ts
  • packages/cli/src/commands/impl/steps/__tests__/handoff-claude.test.ts
  • packages/cli/src/commands/impl/steps/__tests__/handoff-codex.test.ts
  • packages/cli/src/commands/impl/steps/handoff-claude.ts
  • packages/cli/src/commands/impl/steps/handoff-codex.ts
  • packages/cli/src/commands/init/__tests__/init-command.test.ts
  • packages/cli/src/commands/init/index.ts
  • packages/cli/src/commands/init/lib/__tests__/setup-prompt.test.ts
  • packages/cli/src/commands/init/lib/setup-prompt.ts
  • packages/cli/src/commands/init/steps/__tests__/install-deps.test.ts
  • packages/cli/src/commands/init/steps/__tests__/install-eql.test.ts
  • packages/cli/src/commands/init/steps/install-deps.ts
  • packages/cli/src/commands/init/steps/install-eql.ts
  • packages/cli/src/commands/init/types.ts
  • packages/cli/src/messages.ts
  • skills/stash-cli/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/cli/src/messages.ts
  • packages/cli/src/commands/impl/steps/handoff-claude.ts
  • packages/cli/src/commands/impl/steps/handoff-codex.ts
  • .changeset/honest-noninteractive-init.md
  • packages/cli/src/commands/init/index.ts

@coderdan

Copy link
Copy Markdown
Contributor Author

Human and AI review (copilot, coderabbit, Claude/Opus and Codex/gpt-5.6-sol)

@coderdan
coderdan merged commit d20e48a into main Jul 19, 2026
10 checks passed
@coderdan
coderdan deleted the feat/honest-noninteractive-init branch July 19, 2026 03:42
@coderdan

Copy link
Copy Markdown
Contributor Author

Tracked by #714.

coderdan added a commit that referenced this pull request Jul 21, 2026
…s, and ship the doctrine where the CLI can find it (#745)

* fix(cli): survive an unwritable .codex/, and inline skills when it happens

Codex sandboxes deny writes under `.codex/`. `installSkills` created its
destination with an unguarded `mkdirSync` sitting directly ABOVE a
per-skill copy loop that was already guarded — so the failure threw past
that fallback and past the caller, aborting the whole handoff step.

Because the skills install runs first, nothing after it ran either: no
AGENTS.md, no .cipherstash/context.json, no .cipherstash/setup-prompt.md.
All five Codex runs of the rc.3 skilltester matrix landed here, and that
report named it the primary driver of the Claude→Codex quality gap (#736).

Two changes.

`installSkills` never throws. Every filesystem step degrades to a warning
and a shorter return, so the caller decides what an empty result means
rather than being skipped entirely.

The Codex handoff falls back to inlining. When the skills cannot be
written, their bodies go into AGENTS.md — project root, writable —
through the same `doctrine-plus-skills` path the editor-agent handoff
already uses for Cursor / Windsurf / Cline. No new mechanism; the
existing one just was not reachable from this branch. The launch prompt
points at wherever the guidance actually ended up.

The fallback is only claimed when there is something to inline. A
stripped build ships no skills at all, so it stays doctrine-only and says
nothing — reporting a fallback that did not happen would be the same
false-success bug #714 and #687 removed elsewhere in init. That is why
`availableSkills` is split out: an empty install result alone cannot
distinguish "could not write" from "nothing to write".

@cipherstash/wizard carries its own copy of `installSkills` with the
identical unguarded `mkdirSync` above the identical guarded copy loop. It
targets `.claude/skills`, so the Codex sandbox case does not apply, but an
unwritable destination crashed it the same way — guarded to match.

Tests: 2 for the mkdirSync guard (including that the caller can still
distinguish unwritable from empty), 6 for the handoff across all three
states — copied, unwritable-so-inlined, and nothing-to-ship. 645 unit +
68 pty E2E pass; typecheck and build clean; 0 biome errors.

Checked per packages/cli/AGENTS.md: no E2E asserts on these strings (the
only codex assertion is the target list, unchanged), and no skill
documents the install location, so neither needed updating.

* fix(cli): make the Codex skills fallback deliver what it claims

Address the 15 findings from the PR #745 review.

The structural change: installSkills returns { copied, failed } instead
of a flat list, so callers distinguish unwritable / stripped / partial
outcomes without a second probe. The Codex handoff inlines exactly the
failed skills — a partial copy inlines the missing ones and the launch
prompt points at both locations.

The production-blocking find: the bundled AGENTS.md doctrine shipped at
dist/commands/init/doctrine, but findBundledDir probes ancestors of the
compiled chunk in dist/bin/ — so every published build wrote the minimal
AGENTS.md stub, and the inline fallback would have inlined nothing while
claiming otherwise. The doctrine now lands at dist/doctrine, like the
skills bundle, and buildAgentsMdBody honours doctrine-plus-skills even
when the doctrine fragment is missing.

Honesty and hardening around the fallback:

- writeArtifacts takes a SkillsDelivery (installed/inlined/failed);
  context.json gains inlinedSkills and setup-prompt.md stops
  mislabelling an unwritable destination as a "stripped build" — on the
  Claude and AGENTS.md handoffs too
- availableSkills filters on SKILL.md, the predicate the inliner
  actually uses, so "inlining N skills" never overstates
- the AGENTS.md upsert (which refuses malformed sentinel pairs) and the
  bundled-file reads degrade to warnings instead of aborting the step
  before .cipherstash/ is written; skills only count as inlined when
  the write landed
- the fallback warning announces the observed recovery instead of
  asserting an unverified cause, and flags a stale .codex/skills/ it
  could not refresh
- a confirmed-then-failed wizard install is recorded in the wizard
  changelog instead of vanishing with the terminal output

Tests pin the degrade-to-warning contract in both packages (the wizard
suite is new), cover the partial-copy split end to end, and replace the
vacuously-true warning assertions. The doctrine's "Where to read more",
the build-agents-md docblock, and root AGENTS.md now describe the real
mode consumers.

* test(cli): make the partial-copy fixture deterministic across platforms

The read-only-directory trigger was platform-dependent: on macOS cpSync
unlinks the destination file first (EACCES from the 0o555 parent), but
on Linux it overwrites the writable SKILL.md in place and the copy
succeeds — so the test failed on CI while passing locally.

Block the skill with a FILE where its directory must go instead:
cpSync then throws ERR_FS_CP_DIR_TO_NON_DIR, a type-mismatch error
raised before any permission-dependent operation, so it fires on every
platform and as root. Same technique the neighbouring mkdir test
already uses. Drops the skipIf guard the chmod approach needed.

* style(cli): format the partial-copy fixture line
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants